﻿using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using LucyGrid.Messaging;

namespace LucyGrid
{
	/// <summary>
	/// Muticast communication.
	/// Allows the grid to communicate with the nodes.
	/// <remarks>
	/// http://www.codeproject.com/Articles/1705/IP-Multicasting-in-C
	/// </remarks>
	/// </summary>
	public class GridComm : IDisposable
	{
		/// <summary>
		/// Multicast port
		/// </summary>
		private const int GridPort = 11000;
		/// <summary>
		/// End flag
		/// </summary>
		static bool done = false;
		/// <summary>
		/// Receive socket
		/// </summary>
		static UdpClient listener;
		/// <summary>
		/// Send socket
		/// </summary>
		static UdpClient writter;
		/// <summary>
		/// GridComm instances
		/// </summary>
		static List<GridComm> instances;
		/// <summary>
		/// Ip to use for communications
		/// </summary>
		static public IPAddress ip;

		/// <summary>
		/// Identifier to use for communicating
		/// </summary>
		public static readonly string Id;

		public delegate void MessageReceivedHandler(string message);
		public event MessageReceivedHandler OnMessageReceived;

		public delegate void EventReceivedHandler(GridCommEvents eventName, JObject ev);
		public event EventReceivedHandler OnEventReceived;

		public delegate void UnknownEventReceivedHandler(string eventName, JObject ev);
		public event UnknownEventReceivedHandler OnUnknownEventReceived;

		/// <summary>
		/// 
		/// </summary>
		static GridComm()
		{
			instances = new List<GridComm>();
			Id = string.Format("{0}:{1}", Environment.MachineName, Process.GetCurrentProcess().Id);
		}

		public static void Initialize()
		{
			// Let clients try to open as max connections possible againts the grid master
			// Grid master not affected by this setting
			ServicePointManager.DefaultConnectionLimit = int.MaxValue;
			// 100-Continue mechanism not used
			ServicePointManager.Expect100Continue = false;
			// ServicePointManager.MaxServicePoints o by default. No limit
			// ServicePointManager.MaxServicePoints = 0;
		}

		/// <summary>
		/// Returns de writter socket ondemand
		/// </summary>
		public UdpClient Writter
		{
			get
			{
				if (writter == null)
					CreateWritter();
				return writter;
			}
		}

		/// <summary>
		/// Default constructor using 224.5.6.7 multicast IP
		/// </summary>
		public GridComm()
			: this(IPAddress.Parse("224.5.6.7"))
		{

		}
		/// <summary>
		/// Constructor specifying the multicast IP
		/// </summary>
		/// <param name="ip"></param>
		public GridComm(IPAddress ip)
		{
			GridComm.ip = ip;
			lock (instances)
				instances.Add(this);
		}

		/// <summary>
		/// Clean up
		/// </summary>
		public void Dispose()
		{
			lock (instances)
				instances.Remove(this);
			if (instances.Count == 0)
			{
				done = true;
				if (listener != null)
				{
					listener.Close();
				}
				if (writter != null)
					writter.Close();
				// Reset
				listener = null;
				writter = null;
			}
		}

		private const int UDP_BUFFER_SIZE = 100*1024;
		/// <summary>
		/// UDP message buffer
		/// </summary>
		static readonly byte[] udpBytes = new byte[UDP_BUFFER_SIZE]; /* 64Kb message */
		/// <summary>
		/// Create listener to receive events/messages
		/// </summary>
		static void CreateListener()
		{
			if (listener != null)
				return;
			var socket = listener = new UdpClient();
				/*new Socket(AddressFamily.InterNetwork, SocketType.Dgram,
						ProtocolType.Udp);*/
			socket.Client.ReceiveBufferSize = UDP_BUFFER_SIZE;
			socket.Ttl = 128;
			socket.Client.ExclusiveAddressUse = false;
			
			socket.Client.SetSocketOption(SocketOptionLevel.Socket,
					SocketOptionName.ReuseAddress, 1);
			socket.Client.SetSocketOption(SocketOptionLevel.IP,
				SocketOptionName.AddMembership,
					new MulticastOption(ip, IPAddress.Any));
			IPEndPoint ipep = new IPEndPoint(IPAddress.Any, GridPort);
			socket.Client.Bind(ipep);
			
			
		}

		/// <summary>
		/// Create writter to send events/messages
		/// </summary>
		static void CreateWritter()
		{
			if (writter != null)
				return;
			var socket = writter = new UdpClient();
				/*new Socket(AddressFamily.InterNetwork,
				SocketType.Dgram, ProtocolType.Udp);*/

			socket.Client.ReceiveBufferSize = UDP_BUFFER_SIZE;
			socket.Ttl = 128;
			socket.Client.ExclusiveAddressUse = false;

			socket.Client.SetSocketOption(SocketOptionLevel.Socket,
					SocketOptionName.ReuseAddress, 1);
			socket.Client.SetSocketOption(SocketOptionLevel.IP,
				SocketOptionName.AddMembership,
					new MulticastOption(ip, IPAddress.Any));
			socket.Client.SetSocketOption(SocketOptionLevel.IP,
				SocketOptionName.MulticastTimeToLive, 2);
			
			writeripep = new IPEndPoint(ip, GridPort);
			socket.Client.Bind(writeripep);
		}

		/// <summary>
		/// Start receiving
		/// <param name="eventHandler">Message handler to setup OnMessageReceived</param>
		/// <param name="msgHandler">Event handler to setup OnEventReceived</param>
		/// </summary>
		public GridComm StartListener(MessageReceivedHandler msgHandler = null, EventReceivedHandler eventHandler = null)
		{
			if (msgHandler != null)
				OnMessageReceived = msgHandler;
			if (eventHandler != null)
				OnEventReceived = eventHandler;

			if (listener == null)
			{
				CreateListener();
				Listen();
			}
			return this;
		}

		/// <summary>
		/// Listen to UPD messages
		/// </summary>
		static void Listen()
		{
			var queue = new BlockingCollection<byte[]>();
			GridTask.Run(() =>
			{
				var ep = new IPEndPoint(IPAddress.Any, GridPort);
				while (!done)
				{
					var data = listener.Receive(ref ep);
					// Array.Copy(udpBytes, data, data.Length);
					queue.Add(data);
				}
				queue.CompleteAdding();
			});
			GridTask.Run(() =>
			{
				while (!queue.IsCompleted)
				{
					var data = queue.Take();
					string msg = Encoding.UTF8.GetString(data, 0, data.Length);
					ProcessMessageReceived(msg);
				}
			});
		}

		/// <summary>
		/// Process received data
		/// </summary>
		/// <param name="bytes"></param>
		static void ProcessMessageReceived(string data)
		{
			var evValues = Enum.GetNames(typeof(GridCommEvents));
			GridTask.Run(() =>
			{
				Thread.CurrentThread.Name = string.Format("GRIDCOMM MSG-RECEIVER-{0}", Thread.CurrentThread.ManagedThreadId);
				string channel = null;
				JObject jdata = null;
				var knownChannel = true;

				if (data.Length == 0)
					return;

				try
				{
					// Check if it's an event {'Event':{}}
					if (data[0] == '{')
					{
						var json = (JObject)GridUtils.JsonDeserialize(data);
						channel = json["channel"].ToString();
						jdata = (JObject)json["data"];
						knownChannel = Array.IndexOf(evValues, channel) != -1;
					}
				}
				catch (Exception ex)
				{
					GridUtils.NotifyLog("COMMON,INFO", "Unknown event received. {0}", ex);
				}
				// Notify all instances
				lock (instances)
				{
					instances.All(i =>
					{
						try
						{
							if (i.OnMessageReceived != null)
								i.OnMessageReceived(data);

							if (knownChannel)
							{
								if (i.OnEventReceived != null)
								{
									GridCommEvents evName = (GridCommEvents)Enum.Parse(typeof(GridCommEvents), channel);
									i.OnEventReceived(evName, jdata);
								}
							}
							else if (i.OnUnknownEventReceived != null)
							{
								i.OnUnknownEventReceived(channel, jdata);
							}
						}
						catch (Exception ex)
						{
							GridUtils.NotifyLog("COMMON,INFO", "Error procesing message: {0}", ex);
						}
						return true;
					});
				}
			});
		}

		/// <summary>
		/// Publish a message
		/// </summary>
		/// <param name="message"></param>
		/// <param name="values"></param>
		public void SendMessage(string tags, string message, params object[] values)
		{
			try
			{
				var msg = values.Length != 0 ? string.Format(message, values) : message;
				tags = "DEBUG" + (string.IsNullOrEmpty(tags) ? string.Empty : "," + tags);
				GridUtils.NotifyLog(tags, msg);
				var data = UTF8Encoding.UTF8.GetBytes(msg);
				lock (Writter)
					Writter.Send(data, data.Length, writeripep);
			}
			catch (Exception ex)
			{
				GridUtils.NotifyLog("COMMON,INFO", "Error sending multicast message: {0}", ex.ToString());
			}
		}

		/*/// <summary>
		/// Emit an event. It will be send in JSON format: { channel: '', data: {}}
		/// </summary>
		/// <param name="eventName"></param>
		/// <param name="eventData"></param>
		public void Emit(GridCommEvents ev, object eventData)
		{
			Emit(ev.ToString(), eventData);
		}*/
		/// <summary>
		/// Emit an event. It will be send in JSON format: { channel: '', data: {}}
		/// </summary>
		/// <param name="eventName"></param>
		/// <param name="eventData"></param>
		public void Emit(string tags, object eventName, dynamic eventData)
		{
			var msg = GridUtils.JsonSerialize(new { channel = eventName.ToString(), data = eventData });
			SendMessage(tags, msg);
		}

		/// <summary>
		/// Send an stop all notification
		/// </summary>
		public void EmitStopAll()
		{
			EmitMaintenance(GridCommMacros.all, GridCommMaintenaceEvent.stop);
		}

		/// <summary>
		/// Send a maintenace notification to a node
		/// </summary>
		public void EmitMaintenance(object targetMachine, GridCommMaintenaceEvent cmd)
		{
			var o = new Maintenance(){ Tags = "MAINTENANCE", MachineName = targetMachine.ToString(), Cmd = cmd.ToString() };
			Emit(o.Tags, GridCommEvents.maintenance, o);
		}

		public void EmitHeartbeat(Heartbeat heartbeat)
		{
			Emit("MAINTENANCE", GridCommEvents.heartbeat, heartbeat);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="message"></param>
		/// <param name="isError"></param>
		public void EmitLog(string message, bool isError)
		{
			if (!isError) return;
			var log = new
			{
				tags = "ERROR",
				from = string.Format("{0}:{1}", Environment.MachineName, System.Diagnostics.Process.GetCurrentProcess().Id),
				isError,
				message
			};
			Emit(log.tags, GridCommEvents.log, log);
		}

		public class GridWebClient : WebClient {

			protected override WebRequest GetWebRequest(Uri address)
			{
				var req = (HttpWebRequest)base.GetWebRequest(address);
				req.Headers.Add("id", GridComm.Id);
				return req;
			}
		}

		/// <summary>
		/// Http wapper
		/// </summary>
		/// <param name="url"></param>
		/// <param name="writter"></param>
		/// <param name="mode">POST/GET/...</param>
		/// <param name="reader"></param>
		public static bool Http(string url, Action<HttpWebRequest> init = null,
											Action<HttpWebRequest, Stream> send = null,
											Action<HttpWebResponse, Stream> receive = null,
											Action<Exception> error = null)
		{
			try
			{
				var http = (HttpWebRequest)HttpWebRequest.Create(url);
				http.Proxy = null;
				http.KeepAlive = true;
				http.Headers.Add("id", GridComm.Id);
				http.Method = "GET";
				if (init != null)
					init(http);
				if (send != null)
					using (var s = http.GetRequestStream())
					{
						send(http, s);
						s.Close();
					}
				HttpGetResponse(http, receive, error);
			}
			catch (Exception ex)
			{
				if (error != null)
					error(ex);
				return false;
			}
			return true;
		}

		static void HttpGetResponse(HttpWebRequest http,
													Action<HttpWebResponse, Stream> receive,
													Action<Exception> error)
		{
			using (var res = (HttpWebResponse)http.GetResponse())
			{
				if (res.StatusCode != HttpStatusCode.OK)
					throw new Exception("Invalid Reponse Code " + res.StatusCode);
				using (var s = res.GetResponseStream())
				{
					if (receive != null)
						receive(res, s);
				}
			}
		}

		internal static string ResolveName(string host)
		{
			return Dns.GetHostEntry(host).AddressList.First(a => a.AddressFamily == AddressFamily.InterNetwork).ToString();
		}

		public static IPEndPoint writeripep { get; set; }
	}
}
